// Copyright 2013 Ronald Schlenker, www.technewlogic.de.
// 
// This file is part of Byond.
// 
// Byond is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 2 of the License, or
// (at your option) any later version.
// 
// Byond is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
// GNU General Public License for more details.
// 
// You should have received a copy of the GNU General Public License
// along with Byond.  If not, see <http://www.gnu.org/licenses/>.

using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using Byond.EnsembleLibrary;

namespace Byond.Compilation
{
	internal class PatchClassPreparer
	{
		public PatchClassPreparer(Ensemble ensemble)
		{
			_ensemble = ensemble;
		}

		private readonly Library _library;
		private readonly Ensemble _ensemble;
		private readonly IEnsembleQueryService _ensembleQueryService;

		private readonly List<Include> _includes = new List<Include>();
		private readonly List<Include> _processedIncludes = new List<Include>();

		private readonly List<string> _imports = new List<string>();
		public IEnumerable<string> Imports { get { return _imports; } }

		private readonly List<SourceFile> _sourceCodeFiles = new List<SourceFile>();
		public IEnumerable<SourceFile> SourceCodeFiles { get { return _sourceCodeFiles; } }

		public string RootPatchName { get; private set; }

		public void GeneratePatch()
		{
			RootPatchName = _ensemble.FileName.GetClassName();
			var nameSpace = _ensemble.FileName.GetNameSpace(_library.LibraryDirectory);
			PrepareSourceCode(_ensemble.PatchCode, _ensemble.FileName, RootPatchName, nameSpace);

			ProcessIncludes();
		}

		private void ProcessIncludes()
		{
			foreach (var it in _includes.ToList())
			{
				// TODO: Geht das auch mit Containes?
				if (_processedIncludes.Any(it2 => it2.Equals(it)))
					continue;
				_processedIncludes.Add(it);
				//var ensemble = _allEnsembles.SingleOrDefault(it2 => it2.FileName.Equals(it.FullFileName, StringComparison.InvariantCultureIgnoreCase));
				var ensemble = _ensembleQueryService.TryGetByFileName(it.FullFileName);
				if (ensemble == null)
					throw new Exception("Include '" + it.FullFileName + "' could not be found.");
				PrepareSourceCode(ensemble.PatchCode, ensemble.FileName, it.ClassName, it.Namespace);
				ProcessIncludes();
			}
		}

		private void PrepareSourceCode(string sourceCode, string ensembleFileName, string className, string nameSpace)
		{
			// Process the Constructor
			string ctorCode, classCode;
			SplitClassAndCtorCode(sourceCode, out ctorCode, out classCode);

			// scan for includes, imports, usings, new-statements
			IEnumerable<string> tmpIncludes;
			IEnumerable<string> myImports;
			IEnumerable<string> myUsings;
			IEnumerable<string> myNewStatements;
			var processedSourceCode = ParseAndProcessDirectives(classCode, "#i", out tmpIncludes);
			var myIncludes = tmpIncludes.Select(it => new Include(it, ensembleFileName, _library.LibraryDirectory)).ToList();
			processedSourceCode = ParseAndProcessDirectives(processedSourceCode, "#u", out myUsings);
			processedSourceCode = ParseAndProcessDirectives(processedSourceCode, "#r", out myImports);
			processedSourceCode = ParseAndProcessDirectives(processedSourceCode, "#new", out myNewStatements);

			// TODO: new statements evtl. wieder einbauen
			if (myNewStatements.Any())
				throw new Exception("#new statements are not supported yet.");

			var newStatementDeclarations = myNewStatements
				.Aggregate(string.Empty, (a, b) => a + "public " + b + "\r\n");
			var newStatementInitializations = myNewStatements
				.Aggregate(string.Empty, (a, b) => a + "public " + b + ";\r\n");
			var usings = myUsings
				.Concat(myIncludes.Select(it => it.Namespace))
				.Distinct()
				.Aggregate(string.Empty, (a, b) => a + "using " + b + ";\r\n");

			var completeSourceCode = string.Format(@"
using System;
using System.Diagnostics;
using System.Collections.Generic;
using System.Linq;
using System.IO;
using System.Windows.Input;
using System.Threading.Tasks;
using Byond;
using Byond.HostObjects;
using Byond.Midi;
using Byond.Builtin;
using Byond.Persistence;
{0}

namespace {1}
{{
	[PatchMetadataAttribute(@""{7}"")]
	sealed class {2} : Patch
	{{
		{5}

		public {2}(Patch parent = null, string memberName = null, bool registerPatch = false)
			: base(parent, memberName, registerPatch)
		{{
		}}
		
		public override void Go()
		{{
			{6}
			{3}
		}}

		{4}
	}}
}}",
			usings,
			nameSpace,
			className,
			ctorCode,
			processedSourceCode,
			newStatementDeclarations,
			newStatementInitializations,
			ensembleFileName);

			// Write the temp files for compilation
			var tempFileName = Path.GetTempFileName();
			File.WriteAllText(tempFileName, completeSourceCode);
			_sourceCodeFiles.Add(new SourceFile(tempFileName, className));
			_imports.AddRange(myImports);
			_includes.AddRange(myIncludes);
		}

		private static void SplitClassAndCtorCode(string sourceCode, out string ctorCode,
			out string classCode)
		{
			ctorCode = string.Empty;
			classCode = string.Empty;
			using (var sr = new StringReader(sourceCode))
			{
				while (true)
				{
					var line = sr.ReadLine();
					if (line == null)
						break;
					// ignore empty lines
					if (line.Trim().Split(new[] { ' ' }).FirstOrDefault() == "#go")
						ctorCode = sr.ReadToEnd();
					else
						classCode += line + "\r\n";
				}
			}
		}

		private static string ParseAndProcessDirectives(
			string sourceCode,
			string directive,
			out IEnumerable<string> directives)
		{
			var myDirectives = new List<string>();
			var processedSourceCode = string.Empty;
			using (var sr = new StringReader(sourceCode))
			{
				var isInCommentBlock = false;
				while (true)
				{
					var line = sr.ReadLine();
					if (line == null)
						break;
					line = line.Trim();
					// ignore empty or comment lines
					if (line.StartsWith("/*"))
						isInCommentBlock = true;
					// Nicht so ganz - was ist mit dem Code danach? Naja.. :)
					if (line.EndsWith("*/"))
					{
						isInCommentBlock = false;
						continue;
					}
					if (line == string.Empty || line.StartsWith("//") || isInCommentBlock)
						continue;
					if (line.StartsWith(directive))
					{
						var directiveText = line.Remove(0, directive.Length).Trim();
						if (!string.IsNullOrEmpty(directiveText))
							myDirectives.Add(directiveText);
					}
					else if (line.StartsWith("#"))
						processedSourceCode += line + "\r\n";
					else
					{
						// End of directives
						processedSourceCode += line + "\r\n" + (sr.ReadToEnd() ?? string.Empty);
					}
				}
			}
			directives = myDirectives;
			return processedSourceCode;
		}
	}
}